flask中经典错误 working outside application context
在上节我们通过db.create_all(app=app)
的方式解决了working outside application context
的错误,下面我们来深究,这个错误出现的具体原因是什么。
首先来个测试代码:
1 | from flask import Flask, current_app |
我们通过current_app
获取配置,看似没有问题的代码,却抛出了同样的异常。
通过断点调试发现current_app
并不是Flask对象,而是一个unbound
的LocalProxy
。
回想我们之前的request
对象,其实也是个LocalProxy
。
1 | # context locals |
那么这里为什么会抛出这个异常呢,想要回答这个问题,就需要深入理解这个LocalProxy
。我们在下一小节进行介绍
AppContext、RequestContext、Flask与Request之间的关系
1.定位AppContext、RequestContext
Flask
有两个上下文,应用上下文-AppContext
和请求上下文-RequestContext
。他们本质都是对象,是一种封装。应用上下文是对Flask
的封装,请求上下文是对Request
的封装
下面我们来通过源码,了解一下这两个上下文。Flask
源码的全貌,是在External Libraries/site-pacages/flask
下
Flask
是一个非常好的微框架,里面的源码并不多,大部分都是注释,这给我们可以很方便的阅读源码.
我们要看的两个上下文在ctx.py
(context的缩写)中,其中的AppContext
就是应用上下文,RequestContext
就是请求上下文
阅读AppContext
和RequestContext
的构造函数,发现他们都将核心对象app
作为了他们的一个属性
1 | def __init__(self, app): |
并且他们都有相同的四个方法
1 | def push(self): |
2.为什么需要上下文
为什么需要上下文,我们之间操作Flask
的核心对象app
不可以吗?
这是一个设计思想。有时候呢,我们不光需要这个核心对象app
,还需要他外部的一些东西,这个时候,我们可以把他们统一结合封装到一起,组装成一个新的上下文对象,并且在这个对象之上,可以提供一些新的方法,如我们上面所提到的push
、pop
等
3.对AppContext、RequestContext、Flask与Request的意义做出一个解释
Flask:核心对象,核心对象里承载了各种各样的功能,比如保存配置信息,再比如注册路由试图函数等
AppContext:对Flask的封装,并且增加了一些额外的参数
Request:保存了请求信息,比如url的参数,url的全路径等信息
RequestContext:对Request的封装
我们在实际编码过程中,可能是需要Flask
或者Request
的信息的,但是这并不代表我们应该直接导入这两个对象获取相关信息,
正确的做法是从AppContext
,RequestContext
中间接的获得我们需要的信息
即使这样,我们也没有必要导入Context
去使用上下文,这就回到了current_app
和request
这些LocalProxy
,他们提供了间接操作上下文对象的能力,使用了代理模式
详解flask上下文与出入栈
Flask工作原理
当一个请求进入
Flask
框架,首先会实例化一个Request Context
,这个上下文封装了请求的信息在Request
中,并将这个上下文推入到一个栈(_request_ctx_stack/_app_ctx_strack)
的结构中,即之前将的push
方法RequestContext
在入_request_ctx_stack
之前,首先会检查_app_ctx_strack
是否为空,如果为空,则会把一个AppContext
的对象入栈,然后再将这个请求入栈到_request_ctx_stack
中我们的
current_app
和request
对象都是永远指向_app_ctx_strack/_request_ctx_stack
的栈顶元素,也就是分别指向了两个上下文,如果这两个值是空的,那么LocalProxy
就会出现unbound
的状态当请求结束的时候,这个请求会出栈-pop
回到我们之前的测试代码,如果要想让我们的测试代码正常运行,就需要手动将一个AppContext入栈。
1 | from flask import Flask, current_app |
注意
虽然current_app
和request
指向的是两个上下文,但是他们返回的却是Flask核心独享和Request
对象。下面来看下这部分的源码
globals.py
1 | # globals.py中实例化LocalProxy获取current_app的代码中,传入了一个_find_app方法 |
从源码中可以看到,他获取的是app核心对象。
flask上下文与with语句
我们上一小节通过手动将app推入栈,弹出栈的方式,解决了working outside application context
的问题。实际上更经典的做法是使用with语句来完成。
首先使用with语句替换之前的代码
1 | app = Flask(__name__) |
什么时候可以使用with语句:
1.实现了上下文协议的对象,可以使用with语句
2.对于实现了上下文协议的对象,我们通常称为上下文管理员
3.通过实现enter和exit来实现上下文协议
4.上下文表达式必须返回一个上下文管理器
对于上面一段代码来说,AppContext
就是上下文管理器;app.app_context()
就是上下文表达式。enter中做了push操作,__exit__
中做了pop操作。
所以只要进入with语句,current_app
就是有值的,一旦离开了with语句,current_app
就会弹出,然后就又没有值了(又变成了unbound)。
1 | def __enter__(self): |
通过数据库的链接和释放来理解with语句的具体含义
连接数据库的操作步骤:
- 1.连接数据库
- 2.sql或者其他的业务逻辑
- 3.释放资源
如果上面的第二部分出错,那么第三部分的释放资源就不会被执行,资源就会一直被占用。
解决这个问题的通常做法是使用try-except-finally
但是在finally
中更优雅的方式就是使用with
语句中。
我们可以把连接数据库的操作写在上下文管理器的__enter__
方法里面,把业务代码写在with语句的代码块里面,把释放资源的语句写在__exit__
里面。
读写文件的具体例子
一般的写法
1 | try: |
使用with语句的写法:
1 | with open(r'/Users/test.txt') as f: |
注意上面的with语句后面的as 返回的并不是上下文管理器,他实际上是enter方法返回的一个值,
上面一段代码我们在enter中返回了一个a,所以下面as 后的obj_A就是1
exit方法详解
注意我们编写的测试代码,运行时会报错的,错误原因是exit
方法接受的参数数量不够。exit
方法的作用不只是释放资源,还有处理异常,所以exit方法还要多接受exc_type
,exc_value
,tb
三个参数。这三个参数在没有异常发生的时候回传控制,如果有异常的话,这三个参数分别是异常类型,异常消息,和详细的异常堆栈信息
exit
方法还需要返回一个boolean
类型的值,如果返回True
,那么外部就不会抛出异常,如果返回False
,那么还会在外部抛出异常,如果什么都不返回,按照False
处理。Flask
提供了一种非常灵活的方式,可以让我们选择在with语句内部还是外部处理异常
4.6 阅读源码解决db.create_all的问题
对于Flask来说,文档更适合中高级的开发者,而对于新手不是特别友好。所以以不变应万变。我们可以遇到问题的时候,可以通过阅读源码的时候来解决。
下面我们来看下在第三章的时候,为什么我们的flask_sqlalchemy
已经注册了app对象,但是create_all
方法还是需要传入app参数,不传就会报错
首先看一下init_app方法的源码
1 | def init_app(self, app): |
create_app 方法的源码
1 | def _execute_for_all_tables(self, app, bind, operation, skip_tables=False): |
可以看到create_all
方法调用了_execute_for_all_tables
私有方法,_execute_for_all_tables
里面第一行的get_app
方法用来获取一个app
核心对象
1 | def get_app(self, reference_app=None): |
所以通过三个判断,我们可以总结出三个不同的方法来解决我们遇到的问题。
1.在create_all
中传入关键字参数app
。也就是我们之前用过的。
2.向堆栈中推入一条app_context
,使得current_app
不为空。
1 | with app.app_context(): |
3.在初始化flask_sqlalchemy
对象的时候,传入app
参数。
具体选取哪种方式,是根据情况而定的,比如我们当前的情况,就不合适使用第三种方法,因为我们的flask_sqlalchemy
对象是在models
中的book.py
中的,如果用第三种方式,还需要在这里导入app
对象。